Micron Document
NexusPi Git Node

Node / mirrors / RTNode-HeltecV4 / files / BoundaryConfig.h

Displaying Raw • Download

BoundaryConfig.h 5e0e3f538af6523b3c1960013638692061d46a61 (5e0e3f53) Text, 19.32 KB

T8b949e// Copyright (C) 2026, Boundary Mode Extension
T8b949e// Based on microReticulum_Firmware by Mark Qvist
T8b949e//
T8b949e// BoundaryConfig.h — Captive-portal web configuration for Boundary Mode.
T8b949e// When triggered (first boot with no config, or button hold >5s),
T8b949e// the device starts a WiFi AP with a web form for all settings:
T8b949e// WiFi STA credentials, TCP backbone params, LoRa radio params,
T8b949e// and optional AP-mode TCP server.
T8b949e//
T8b949e// This program is free software: you can redistribute it and/or modify
T8b949e// it under the terms of the GNU General Public License as published by
T8b949e// the Free Software Foundation, either version 3 of the License, or
T8b949e// (at your option) any later version.

Tff7b72#Tff7b72ifndef BOUNDARY_CONFIG_H
Tff7b72#Tff7b72define BOUNDARY_CONFIG_H

Tff7b72#Tff7b72ifdef BOUNDARY_MODE

Tff7b72#Tff7b72include T8b949e<WiFi.h>
Tff7b72#Tff7b72include T8b949e<WebServer.h>
Tff7b72#Tff7b72include T8b949e<DNSServer.h>

T8b949e// ─── Config Portal State ─────────────────────────────────────────────────────
Tff7b72static Tffa657bool Te6edf3config_portal_active Tff7b72= Tffa657falseTb4b4b4;
Tff7b72static Te6edf3WebServerTff7b72* Te6edf3config_server Tff7b72= Tffa657nullptrTb4b4b4;
Tff7b72static Te6edf3DNSServerTff7b72* Te6edf3config_dns Tff7b72= Tffa657nullptrTb4b4b4;

Tff7b72static Tff7b72const Tffa657char Te6edf3CONFIG_AP_SSIDTb4b4b4[Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ffRNode-Boundary-SetupTa5d6ff"Tb4b4b4;
Tff7b72static Tff7b72const Tffa657uint16_t Te6edf3DNS_PORT Tff7b72= T79c0ff53Tb4b4b4;
Tff7b72static Tff7b72const Tffa657uint16_t Te6edf3HTTP_PORT Tff7b72= T79c0ff80Tb4b4b4;

T8b949e// Forward declarations
Tffa657void Td2a8ffconfig_portal_startTb4b4b4(Tb4b4b4)Tb4b4b4;
Tffa657void Td2a8ffconfig_portal_stopTb4b4b4(Tb4b4b4)Tb4b4b4;
Tffa657void Td2a8ffconfig_portal_loopTb4b4b4(Tb4b4b4)Tb4b4b4;
Tffa657bool Td2a8ffconfig_portal_is_activeTb4b4b4(Tb4b4b4)Tb4b4b4;
Tffa657bool Td2a8ffboundary_needs_configTb4b4b4(Tb4b4b4)Tb4b4b4;

T8b949e// ─── Common bandwidth values (Hz) ───────────────────────────────────────────
T8b949e// These match Reticulum standard channel plans
Tff7b72struct T56d364BwOption Tb4b4b4{ Tffa657uint32_t Te6edf3hzTb4b4b4; Tff7b72const Tffa657charTff7b72* Te6edf3labelTb4b4b4; Tb4b4b4}Tb4b4b4;
Tff7b72static Tff7b72const Te6edf3BwOption Te6edf3BW_OPTIONSTb4b4b4[Tb4b4b4] Tff7b72= Tb4b4b4{
Tb4b4b4{ T79c0ff7800Tb4b4b4, Ta5d6ff"Ta5d6ff7.8 kHzTa5d6ff" Tb4b4b4}Tb4b4b4,
Tb4b4b4{ T79c0ff10400Tb4b4b4, Ta5d6ff"Ta5d6ff10.4 kHzTa5d6ff" Tb4b4b4}Tb4b4b4,
Tb4b4b4{ T79c0ff15600Tb4b4b4, Ta5d6ff"Ta5d6ff15.6 kHzTa5d6ff" Tb4b4b4}Tb4b4b4,
Tb4b4b4{ T79c0ff20800Tb4b4b4, Ta5d6ff"Ta5d6ff20.8 kHzTa5d6ff" Tb4b4b4}Tb4b4b4,
Tb4b4b4{ T79c0ff31250Tb4b4b4, Ta5d6ff"Ta5d6ff31.25 kHzTa5d6ff" Tb4b4b4}Tb4b4b4,
Tb4b4b4{ T79c0ff41700Tb4b4b4, Ta5d6ff"Ta5d6ff41.7 kHzTa5d6ff" Tb4b4b4}Tb4b4b4,
Tb4b4b4{ T79c0ff62500Tb4b4b4, Ta5d6ff"Ta5d6ff62.5 kHzTa5d6ff" Tb4b4b4}Tb4b4b4,
Tb4b4b4{T79c0ff125000Tb4b4b4, Ta5d6ff"Ta5d6ff125 kHzTa5d6ff" Tb4b4b4}Tb4b4b4,
Tb4b4b4{T79c0ff250000Tb4b4b4, Ta5d6ff"Ta5d6ff250 kHzTa5d6ff" Tb4b4b4}Tb4b4b4,
Tb4b4b4{T79c0ff500000Tb4b4b4, Ta5d6ff"Ta5d6ff500 kHzTa5d6ff" Tb4b4b4}Tb4b4b4,
Tb4b4b4}Tb4b4b4;
Tff7b72static Tff7b72const Tffa657int Te6edf3BW_OPTIONS_COUNT Tff7b72= Tff7b72sizeofTb4b4b4(Te6edf3BW_OPTIONSTb4b4b4) Tff7b72/ Tff7b72sizeofTb4b4b4(Te6edf3BW_OPTIONSTb4b4b4[T79c0ff0Tb4b4b4]Tb4b4b4)Tb4b4b4;

T8b949e// ─── HTML Page Generation ────────────────────────────────────────────────────

Tff7b72static Tffa657void Td2a8ffconfig_send_htmlTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Read current values from EEPROM/globals for pre-population
Tffa657char Te6edf3cur_ssidTb4b4b4[T79c0ff33Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ff"Tb4b4b4;
Tffa657char Te6edf3cur_pskTb4b4b4[T79c0ff33Tb4b4b4] Tff7b72= Ta5d6ff"Ta5d6ff"Tb4b4b4;

Tff7b72for Tb4b4b4(Tffa657int Te6edf3i Tff7b72= T79c0ff0Tb4b4b4; Te6edf3i Tff7b72< T79c0ff32Tb4b4b4; Te6edf3iTff7b72+Tff7b72+Tb4b4b4) Tb4b4b4{
Te6edf3cur_ssidTb4b4b4[Te6edf3iTb4b4b4] Tff7b72= Te6edf3EEPROMTb4b4b4.Te6edf3readTb4b4b4(Te6edf3config_addrTb4b4b4(Te6edf3ADDR_CONF_SSID Tff7b72+ Te6edf3iTb4b4b4)Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3cur_ssidTb4b4b4[Te6edf3iTb4b4b4] Tff7b72=Tff7b72= Tb4b4b4(Tffa657charTb4b4b4)T79c0ff0xFFTb4b4b4) Te6edf3cur_ssidTb4b4b4[Te6edf3iTb4b4b4] Tff7b72= Ta5d6ff'Ta5d6ff\0Ta5d6ff'Tb4b4b4;
Tb4b4b4}
Te6edf3cur_ssidTb4b4b4[T79c0ff32Tb4b4b4] Tff7b72= Ta5d6ff'Ta5d6ff\0Ta5d6ff'Tb4b4b4;

Tff7b72for Tb4b4b4(Tffa657int Te6edf3i Tff7b72= T79c0ff0Tb4b4b4; Te6edf3i Tff7b72< T79c0ff32Tb4b4b4; Te6edf3iTff7b72+Tff7b72+Tb4b4b4) Tb4b4b4{
Te6edf3cur_pskTb4b4b4[Te6edf3iTb4b4b4] Tff7b72= Te6edf3EEPROMTb4b4b4.Te6edf3readTb4b4b4(Te6edf3config_addrTb4b4b4(Te6edf3ADDR_CONF_PSK Tff7b72+ Te6edf3iTb4b4b4)Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3cur_pskTb4b4b4[Te6edf3iTb4b4b4] Tff7b72=Tff7b72= Tb4b4b4(Tffa657charTb4b4b4)T79c0ff0xFFTb4b4b4) Te6edf3cur_pskTb4b4b4[Te6edf3iTb4b4b4] Tff7b72= Ta5d6ff'Ta5d6ff\0Ta5d6ff'Tb4b4b4;
Tb4b4b4}
Te6edf3cur_pskTb4b4b4[T79c0ff32Tb4b4b4] Tff7b72= Ta5d6ff'Ta5d6ff\0Ta5d6ff'Tb4b4b4;

T8b949e// Current LoRa values (from globals, which were loaded from EEPROM)
Tffa657uint32_t Te6edf3cur_freq Tff7b72= Te6edf3lora_freqTb4b4b4;
Tffa657uint32_t Te6edf3cur_bw Tff7b72= Te6edf3lora_bwTb4b4b4;
Tffa657int Te6edf3cur_sf Tff7b72= Te6edf3lora_sfTb4b4b4;
Tffa657int Te6edf3cur_cr Tff7b72= Te6edf3lora_crTb4b4b4;
Tffa657int Te6edf3cur_txp Tff7b72= Te6edf3lora_txpTb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3cur_txp Tff7b72=Tff7b72= T79c0ff0xFFTb4b4b4) Te6edf3cur_txp Tff7b72= T79c0ff28Tb4b4b4; T8b949e// Default TX power

T8b949e// Default frequency if not set
Tff7b72if Tb4b4b4(Te6edf3cur_freq Tff7b72=Tff7b72= T79c0ff0Tb4b4b4) Te6edf3cur_freq Tff7b72= T79c0ff914875000Tb4b4b4; T8b949e// 914.875 MHz default
Tff7b72if Tb4b4b4(Te6edf3cur_bw Tff7b72=Tff7b72= T79c0ff0Tb4b4b4) Te6edf3cur_bw Tff7b72= T79c0ff125000Tb4b4b4; T8b949e// 125 kHz default
Tff7b72if Tb4b4b4(Te6edf3cur_sf Tff7b72=Tff7b72= T79c0ff0Tb4b4b4) Te6edf3cur_sf Tff7b72= T79c0ff10Tb4b4b4; T8b949e// SF10 default
Tff7b72if Tb4b4b4(Te6edf3cur_cr Tff7b72< T79c0ff5 Tff7b72|Tff7b72| Te6edf3cur_cr Tff7b72> T79c0ff8Tb4b4b4) Te6edf3cur_cr Tff7b72= T79c0ff5Tb4b4b4; T8b949e// CR 4/5 default

T8b949e// Build the HTML page
Te6edf3String Te6edf3html Tff7b72= Te6edf3FTb4b4b4(
Ta5d6ff"Ta5d6ff<!DOCTYPE html><html><head>Ta5d6ff"
Ta5d6ff"Ta5d6ff<meta name='viewport' content='width=device-width,initial-scale=1'>Ta5d6ff"
Ta5d6ff"Ta5d6ff<title>RNode Boundary Setup</title>Ta5d6ff"
Ta5d6ff"Ta5d6ff<style>Ta5d6ff"
Ta5d6ff"Ta5d6ffbody{font-family:sans-serif;background:#1a1a2e;color:#e0e0e0;margin:0;padding:16px;}Ta5d6ff"
Ta5d6ff"Ta5d6ffh1{color:#e94560;font-size:1.4em;margin:0 0 8px;}Ta5d6ff"
Ta5d6ff"Ta5d6ffh2{color:#0f3460;background:#e0e0e0;padding:6px 10px;margin:18px -10px 10px;font-size:1em;border-radius:4px;}Ta5d6ff"
Ta5d6ff"Ta5d6ffform{max-width:480px;margin:0 auto;}Ta5d6ff"
Ta5d6ff"Ta5d6fflabel{display:block;margin:8px 0 2px;font-size:0.9em;color:#aaa;}Ta5d6ff"
Ta5d6ff"Ta5d6ffinput,select{width:100%;padding:8px;margin:2px 0 6px;box-sizing:border-box;Ta5d6ff"
Ta5d6ff"Ta5d6ffbackground:#16213e;border:1px solid #0f3460;color:#e0e0e0;border-radius:4px;font-size:0.95em;}Ta5d6ff"
Ta5d6ff"Ta5d6ffinput:focus,select:focus{border-color:#e94560;outline:none;}Ta5d6ff"
Ta5d6ff"Ta5d6ff.row{display:flex;gap:10px;}.row>div{flex:1;}Ta5d6ff"
Ta5d6ff"Ta5d6ff.note{font-size:0.8em;color:#666;margin:2px 0 8px;}Ta5d6ff"
Ta5d6ff"Ta5d6ffbutton{width:100%;padding:12px;margin:20px 0;background:#e94560;color:#fff;Ta5d6ff"
Ta5d6ff"Ta5d6ffborder:none;border-radius:4px;font-size:1.1em;cursor:pointer;}Ta5d6ff"
Ta5d6ff"Ta5d6ffbutton:hover{background:#c73e54;}Ta5d6ff"
Ta5d6ff"Ta5d6ff.ok{background:#16213e;padding:20px;border-radius:8px;text-align:center;}Ta5d6ff"
Ta5d6ff"Ta5d6ff.ok h1{color:#0f0;}Ta5d6ff"
Ta5d6ff"Ta5d6ff</style></head><body>Ta5d6ff"
Ta5d6ff"Ta5d6ff<h1>&#x1f4e1; RNode Boundary Node</h1>Ta5d6ff"
Ta5d6ff"Ta5d6ff<form method='POST' action='/save'>Ta5d6ff"
Tb4b4b4)Tb4b4b4;

T8b949e// ── WiFi STA Section ──
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(
Ta5d6ff"Ta5d6ff<h2>&#x1f4f6; WiFi Network</h2>Ta5d6ff"
Ta5d6ff"Ta5d6ff<label>WiFi</label>Ta5d6ff"
Ta5d6ff"Ta5d6ff<select name='wifi_en'>Ta5d6ff"
Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<option value='1'Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3wifi_enabledTb4b4b4) Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff selectedTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff>Enabled</option>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<option value='0'Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3boundary_stateTb4b4b4.Te6edf3wifi_enabledTb4b4b4) Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff selectedTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff>Disabled (LoRa-only repeater)</option>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff</select>Ta5d6ff"Tb4b4b4)Tb4b4b4;

Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(
Ta5d6ff"Ta5d6ff<label>SSID</label>Ta5d6ff"
Ta5d6ff"Ta5d6ff<input name='ssid' maxlength='32' placeholder='Your WiFi network' value='Ta5d6ff"
Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3cur_ssidTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(
Ta5d6ff"Ta5d6ff'>Ta5d6ff"
Ta5d6ff"Ta5d6ff<label>Password</label>Ta5d6ff"
Ta5d6ff"Ta5d6ff<input name='psk' type='password' maxlength='32' placeholder='WiFi password' value='Ta5d6ff"
Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3cur_pskTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff'>Ta5d6ff"Tb4b4b4)Tb4b4b4;

T8b949e// ── TCP Backbone Section ──
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(
Ta5d6ff"Ta5d6ff<h2>&#x1f310; TCP Backbone</h2>Ta5d6ff"
Ta5d6ff"Ta5d6ff<label>Mode</label>Ta5d6ff"
Ta5d6ff"Ta5d6ff<select name='tcp_mode'>Ta5d6ff"
Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<option value='0'Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3tcp_mode Tff7b72=Tff7b72= T79c0ff0Tb4b4b4) Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff selectedTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff>Disabled</option>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<option value='1'Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3tcp_mode Tff7b72=Tff7b72= T79c0ff1Tb4b4b4) Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff selectedTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff>Client (connect to backbone)</option>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff</select>Ta5d6ff"Tb4b4b4)Tb4b4b4;

Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<label>Backbone Host</label>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<input name='bb_host' maxlength='63' placeholder='e.g. 192.168.1.100' value='Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3backbone_hostTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff'>Ta5d6ff"Tb4b4b4)Tb4b4b4;

Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<label>Backbone Port</label>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<input name='bb_port' type='number' min='1' max='65535' value='Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3backbone_portTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff'>Ta5d6ff"Tb4b4b4)Tb4b4b4;

T8b949e// ── Local TCP Server Section ──
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(
Ta5d6ff"Ta5d6ff<h2>&#x1f4e1; Local TCP Server (optional)</h2>Ta5d6ff"
Ta5d6ff"Ta5d6ff<p class='note'>Run a TCP server on the same WiFi network so local devices can connect. Ta5d6ff"
Ta5d6ff"Ta5d6ffUses Access Point mode (does not forward announces).</p>Ta5d6ff"
Ta5d6ff"Ta5d6ff<label>Local TCP Server</label>Ta5d6ff"
Ta5d6ff"Ta5d6ff<select name='ap_tcp_en'>Ta5d6ff"
Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<option value='0'Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3boundary_stateTb4b4b4.Te6edf3ap_tcp_enabledTb4b4b4) Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff selectedTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff>Disabled</option>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<option value='1'Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3ap_tcp_enabledTb4b4b4) Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff selectedTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff>Enabled</option>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff</select>Ta5d6ff"Tb4b4b4)Tb4b4b4;

Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<label>TCP Port</label>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<input name='ap_tcp_port' type='number' min='1' max='65535' value='Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3ap_tcp_portTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff'>Ta5d6ff"Tb4b4b4)Tb4b4b4;

T8b949e// ── LoRa Radio Section ──
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(
Ta5d6ff"Ta5d6ff<h2>&#x1f4fb; LoRa Radio</h2>Ta5d6ff"
Tb4b4b4)Tb4b4b4;

T8b949e// Frequency — show in MHz for human-friendliness
Tffa657char Te6edf3freq_strTb4b4b4[T79c0ff16Tb4b4b4]Tb4b4b4;
Te6edf3dtostrfTb4b4b4(Tb4b4b4(Tffa657doubleTb4b4b4)Te6edf3cur_freq Tff7b72/ T79c0ff1000000.0Tb4b4b4, T79c0ff1Tb4b4b4, T79c0ff3Tb4b4b4, Te6edf3freq_strTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<label>Frequency (MHz)</label>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<input name='freq' type='text' placeholder='914.875' value='Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3freq_strTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff'>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<p class='note'>e.g. 914.875, 868.000, 433.000</p>Ta5d6ff"Tb4b4b4)Tb4b4b4;

T8b949e// Bandwidth — dropdown
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<label>Bandwidth</label><select name='bw'>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tff7b72for Tb4b4b4(Tffa657int Te6edf3i Tff7b72= T79c0ff0Tb4b4b4; Te6edf3i Tff7b72< Te6edf3BW_OPTIONS_COUNTTb4b4b4; Te6edf3iTff7b72+Tff7b72+Tb4b4b4) Tb4b4b4{
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<option value='Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3BW_OPTIONSTb4b4b4[Te6edf3iTb4b4b4]Tb4b4b4.Te6edf3hzTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ff'Ta5d6ff"Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3BW_OPTIONSTb4b4b4[Te6edf3iTb4b4b4]Tb4b4b4.Te6edf3hz Tff7b72=Tff7b72= Te6edf3cur_bwTb4b4b4) Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff selectedTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ff>Ta5d6ff"Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3BW_OPTIONSTb4b4b4[Te6edf3iTb4b4b4]Tb4b4b4.Te6edf3labelTb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff</option>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tb4b4b4}
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff</select>Ta5d6ff"Tb4b4b4)Tb4b4b4;

T8b949e// Spreading Factor — dropdown 6-12
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<label>Spreading Factor</label><select name='sf'>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tff7b72for Tb4b4b4(Tffa657int Te6edf3sf Tff7b72= T79c0ff6Tb4b4b4; Te6edf3sf Tff7b72<Tff7b72= T79c0ff12Tb4b4b4; Te6edf3sfTff7b72+Tff7b72+Tb4b4b4) Tb4b4b4{
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<option value='Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3sfTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ff'Ta5d6ff"Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3sf Tff7b72=Tff7b72= Te6edf3cur_sfTb4b4b4) Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff selectedTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ff>SFTa5d6ff"Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3sfTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff</option>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tb4b4b4}
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff</select>Ta5d6ff"Tb4b4b4)Tb4b4b4;

T8b949e// Coding Rate — dropdown 5-8 (maps to 4/5 through 4/8)
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<label>Coding Rate</label><select name='cr'>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tff7b72for Tb4b4b4(Tffa657int Te6edf3cr Tff7b72= T79c0ff5Tb4b4b4; Te6edf3cr Tff7b72<Tff7b72= T79c0ff8Tb4b4b4; Te6edf3crTff7b72+Tff7b72+Tb4b4b4) Tb4b4b4{
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<option value='Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3crTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ff'Ta5d6ff"Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3cr Tff7b72=Tff7b72= Te6edf3cur_crTb4b4b4) Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff selectedTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ff>4/Ta5d6ff"Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3crTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff</option>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tb4b4b4}
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff</select>Ta5d6ff"Tb4b4b4)Tb4b4b4;

T8b949e// TX Power
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<label>TX Power (dBm)</label>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<input name='txp' type='number' min='2' max='Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tff7b72#Tff7b72ifdef PA_MAX_OUTPUT
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3PA_MAX_OUTPUTTb4b4b4)Tb4b4b4;
Tff7b72#Tff7b72else
Te6edf3html Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ff22Ta5d6ff"Tb4b4b4;
Tff7b72#Tff7b72endif
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff' value='Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3cur_txpTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff'>Ta5d6ff"Tb4b4b4)Tb4b4b4;

Tff7b72#Tff7b72ifdef PA_MAX_OUTPUT
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff<p class='note'>Max output for this board: Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3StringTb4b4b4(Te6edf3PA_MAX_OUTPUTTb4b4b4)Tb4b4b4;
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(Ta5d6ff"Ta5d6ff dBm (with PA)</p>Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tff7b72#Tff7b72endif

T8b949e// ── Submit ──
Te6edf3html Tff7b72+Tff7b72= Te6edf3FTb4b4b4(
Ta5d6ff"Ta5d6ff<button type='submit'>Save &amp; Reboot</button>Ta5d6ff"
Ta5d6ff"Ta5d6ff</form></body></html>Ta5d6ff"
Tb4b4b4)Tb4b4b4;

Te6edf3config_serverTff7b72-Tff7b72>Te6edf3sendTb4b4b4(T79c0ff200Tb4b4b4, Ta5d6ff"Ta5d6fftext/htmlTa5d6ff"Tb4b4b4, Te6edf3htmlTb4b4b4)Tb4b4b4;
Tb4b4b4}

T8b949e// ─── Handle POST /save ──────────────────────────────────────────────────────

Tff7b72static Tffa657void Td2a8ffconfig_handle_saveTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// ── WiFi STA credentials ──
Te6edf3String Te6edf3ssid Tff7b72= Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6ffssidTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3String Te6edf3psk Tff7b72= Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6ffpskTa5d6ff"Tb4b4b4)Tb4b4b4;

T8b949e// Write SSID to config EEPROM area
Tff7b72for Tb4b4b4(Tffa657int Te6edf3i Tff7b72= T79c0ff0Tb4b4b4; Te6edf3i Tff7b72< T79c0ff32Tb4b4b4; Te6edf3iTff7b72+Tff7b72+Tb4b4b4) Tb4b4b4{
Tffa657uint8_t Te6edf3c Tff7b72= Tb4b4b4(Te6edf3i Tff7b72< Tb4b4b4(Tffa657intTb4b4b4)Te6edf3ssidTb4b4b4.Te6edf3lengthTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72? Te6edf3ssidTb4b4b4[Te6edf3iTb4b4b4] Tff7b72: T79c0ff0x00Tb4b4b4;
Te6edf3EEPROMTb4b4b4.Te6edf3writeTb4b4b4(Te6edf3config_addrTb4b4b4(Te6edf3ADDR_CONF_SSID Tff7b72+ Te6edf3iTb4b4b4)Tb4b4b4, Te6edf3cTb4b4b4)Tb4b4b4;
Tb4b4b4}
Te6edf3EEPROMTb4b4b4.Te6edf3writeTb4b4b4(Te6edf3config_addrTb4b4b4(Te6edf3ADDR_CONF_SSID Tff7b72+ T79c0ff32Tb4b4b4)Tb4b4b4, T79c0ff0x00Tb4b4b4)Tb4b4b4;

T8b949e// Write PSK
Tff7b72for Tb4b4b4(Tffa657int Te6edf3i Tff7b72= T79c0ff0Tb4b4b4; Te6edf3i Tff7b72< T79c0ff32Tb4b4b4; Te6edf3iTff7b72+Tff7b72+Tb4b4b4) Tb4b4b4{
Tffa657uint8_t Te6edf3c Tff7b72= Tb4b4b4(Te6edf3i Tff7b72< Tb4b4b4(Tffa657intTb4b4b4)Te6edf3pskTb4b4b4.Te6edf3lengthTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72? Te6edf3pskTb4b4b4[Te6edf3iTb4b4b4] Tff7b72: T79c0ff0x00Tb4b4b4;
Te6edf3EEPROMTb4b4b4.Te6edf3writeTb4b4b4(Te6edf3config_addrTb4b4b4(Te6edf3ADDR_CONF_PSK Tff7b72+ Te6edf3iTb4b4b4)Tb4b4b4, Te6edf3cTb4b4b4)Tb4b4b4;
Tb4b4b4}
Te6edf3EEPROMTb4b4b4.Te6edf3writeTb4b4b4(Te6edf3config_addrTb4b4b4(Te6edf3ADDR_CONF_PSK Tff7b72+ T79c0ff32Tb4b4b4)Tb4b4b4, T79c0ff0x00Tb4b4b4)Tb4b4b4;

T8b949e// Set WiFi mode to STA
Te6edf3EEPROMTb4b4b4.Te6edf3writeTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_WIFITb4b4b4)Tb4b4b4, Te6edf3WR_WIFI_STATb4b4b4)Tb4b4b4;

T8b949e// ── WiFi enable setting ──
Te6edf3boundary_stateTb4b4b4.Te6edf3wifi_enabled Tff7b72= Tb4b4b4(Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6ffwifi_enTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Tff7b72=Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4;

T8b949e// ── TCP backbone settings ──
Te6edf3boundary_stateTb4b4b4.Te6edf3tcp_mode Tff7b72= Tb4b4b4(Tffa657uint8_tTb4b4b4)Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6fftcp_modeTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4)Tb4b4b4; T8b949e// 0=disabled, 1=client
Tff7b72if Tb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3tcp_mode Tff7b72> T79c0ff1Tb4b4b4) Te6edf3boundary_stateTb4b4b4.Te6edf3tcp_mode Tff7b72= T79c0ff0Tb4b4b4;
Te6edf3boundary_stateTb4b4b4.Te6edf3tcp_port Tff7b72= Tb4b4b4(Tffa657uint16_tTb4b4b4)Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6fftcp_portTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3tcp_port Tff7b72=Tff7b72= T79c0ff0Tb4b4b4) Te6edf3boundary_stateTb4b4b4.Te6edf3tcp_port Tff7b72= T79c0ff4242Tb4b4b4;

Te6edf3String Te6edf3bb_host Tff7b72= Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6ffbb_hostTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3memsetTb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3backbone_hostTb4b4b4, T79c0ff0Tb4b4b4, Tff7b72sizeofTb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3backbone_hostTb4b4b4)Tb4b4b4)Tb4b4b4;
Te6edf3strncpyTb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3backbone_hostTb4b4b4, Te6edf3bb_hostTb4b4b4.Te6edf3c_strTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72sizeofTb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3backbone_hostTb4b4b4) Tff7b72- T79c0ff1Tb4b4b4)Tb4b4b4;

Te6edf3boundary_stateTb4b4b4.Te6edf3backbone_port Tff7b72= Tb4b4b4(Tffa657uint16_tTb4b4b4)Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6ffbb_portTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3backbone_port Tff7b72=Tff7b72= T79c0ff0Tb4b4b4) Te6edf3boundary_stateTb4b4b4.Te6edf3backbone_port Tff7b72= T79c0ff4242Tb4b4b4;

T8b949e// ── Local TCP server settings ──
Te6edf3boundary_stateTb4b4b4.Te6edf3ap_tcp_enabled Tff7b72= Tb4b4b4(Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6ffap_tcp_enTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Tff7b72=Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4;
Te6edf3boundary_stateTb4b4b4.Te6edf3ap_tcp_port Tff7b72= Tb4b4b4(Tffa657uint16_tTb4b4b4)Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6ffap_tcp_portTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3boundary_stateTb4b4b4.Te6edf3ap_tcp_port Tff7b72=Tff7b72= T79c0ff0Tb4b4b4) Te6edf3boundary_stateTb4b4b4.Te6edf3ap_tcp_port Tff7b72= T79c0ff4242Tb4b4b4;

T8b949e// Save boundary config to EEPROM
Te6edf3boundary_save_configTb4b4b4(Tb4b4b4)Tb4b4b4;

T8b949e// ── LoRa radio settings ──
Te6edf3String Te6edf3freq_str Tff7b72= Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6fffreqTa5d6ff"Tb4b4b4)Tb4b4b4;
Tffa657double Te6edf3freq_mhz Tff7b72= Te6edf3freq_strTb4b4b4.Te6edf3toDoubleTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3freq_mhz Tff7b72> T79c0ff0Tb4b4b4) Tb4b4b4{
Te6edf3lora_freq Tff7b72= Tb4b4b4(Tffa657uint32_tTb4b4b4)Tb4b4b4(Te6edf3freq_mhz Tff7b72* T79c0ff1000000.0Tb4b4b4)Tb4b4b4;
Tb4b4b4}

Te6edf3String Te6edf3bw_str Tff7b72= Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6ffbwTa5d6ff"Tb4b4b4)Tb4b4b4;
Tffa657uint32_t Te6edf3bw_val Tff7b72= Tb4b4b4(Tffa657uint32_tTb4b4b4)Te6edf3bw_strTb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3bw_val Tff7b72> T79c0ff0Tb4b4b4) Te6edf3lora_bw Tff7b72= Te6edf3bw_valTb4b4b4;

Tffa657int Te6edf3sf_val Tff7b72= Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6ffsfTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3sf_val Tff7b72>Tff7b72= T79c0ff6 Tff7b72&Tff7b72& Te6edf3sf_val Tff7b72<Tff7b72= T79c0ff12Tb4b4b4) Te6edf3lora_sf Tff7b72= Te6edf3sf_valTb4b4b4;

Tffa657int Te6edf3cr_val Tff7b72= Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6ffcrTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3cr_val Tff7b72>Tff7b72= T79c0ff5 Tff7b72&Tff7b72& Te6edf3cr_val Tff7b72<Tff7b72= T79c0ff8Tb4b4b4) Te6edf3lora_cr Tff7b72= Te6edf3cr_valTb4b4b4;

Tffa657int Te6edf3txp_val Tff7b72= Te6edf3config_serverTff7b72-Tff7b72>Te6edf3argTb4b4b4(Ta5d6ff"Ta5d6fftxpTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3txp_val Tff7b72>Tff7b72= T79c0ff2 Tff7b72&Tff7b72& Te6edf3txp_val Tff7b72<Tff7b72= T79c0ff30Tb4b4b4) Te6edf3lora_txp Tff7b72= Te6edf3txp_valTb4b4b4;

T8b949e// Save LoRa config to EEPROM (reuse existing eeprom_conf functions)
T8b949e// Write directly since hw_ready may not be set yet
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_SFTb4b4b4)Tb4b4b4, Te6edf3lora_sfTb4b4b4)Tb4b4b4;
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_CRTb4b4b4)Tb4b4b4, Te6edf3lora_crTb4b4b4)Tb4b4b4;
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_TXPTb4b4b4)Tb4b4b4, Te6edf3lora_txpTb4b4b4)Tb4b4b4;
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_BWTb4b4b4) Tff7b72+ T79c0ff0Tb4b4b4, Te6edf3lora_bw Tff7b72>Tff7b72> T79c0ff24Tb4b4b4)Tb4b4b4;
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_BWTb4b4b4) Tff7b72+ T79c0ff1Tb4b4b4, Te6edf3lora_bw Tff7b72>Tff7b72> T79c0ff16Tb4b4b4)Tb4b4b4;
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_BWTb4b4b4) Tff7b72+ T79c0ff2Tb4b4b4, Te6edf3lora_bw Tff7b72>Tff7b72> T79c0ff8Tb4b4b4)Tb4b4b4;
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_BWTb4b4b4) Tff7b72+ T79c0ff3Tb4b4b4, Te6edf3lora_bwTb4b4b4)Tb4b4b4;
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_FREQTb4b4b4) Tff7b72+ T79c0ff0Tb4b4b4, Te6edf3lora_freq Tff7b72>Tff7b72> T79c0ff24Tb4b4b4)Tb4b4b4;
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_FREQTb4b4b4) Tff7b72+ T79c0ff1Tb4b4b4, Te6edf3lora_freq Tff7b72>Tff7b72> T79c0ff16Tb4b4b4)Tb4b4b4;
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_FREQTb4b4b4) Tff7b72+ T79c0ff2Tb4b4b4, Te6edf3lora_freq Tff7b72>Tff7b72> T79c0ff8Tb4b4b4)Tb4b4b4;
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_FREQTb4b4b4) Tff7b72+ T79c0ff3Tb4b4b4, Te6edf3lora_freqTb4b4b4)Tb4b4b4;
Te6edf3eeprom_updateTb4b4b4(Te6edf3eeprom_addrTb4b4b4(Te6edf3ADDR_CONF_OKTb4b4b4)Tb4b4b4, Te6edf3CONF_OK_BYTETb4b4b4)Tb4b4b4;

Te6edf3EEPROMTb4b4b4.Te6edf3commitTb4b4b4(Tb4b4b4)Tb4b4b4;

T8b949e// ── Send confirmation page ──
Te6edf3String Te6edf3ok Tff7b72= Te6edf3FTb4b4b4(
Ta5d6ff"Ta5d6ff<!DOCTYPE html><html><head>Ta5d6ff"
Ta5d6ff"Ta5d6ff<meta name='viewport' content='width=device-width,initial-scale=1'>Ta5d6ff"
Ta5d6ff"Ta5d6ff<title>Saved</title>Ta5d6ff"
Ta5d6ff"Ta5d6ff<style>Ta5d6ff"
Ta5d6ff"Ta5d6ffbody{font-family:sans-serif;background:#1a1a2e;color:#e0e0e0;padding:40px;Ta5d6ff"
Ta5d6ff"Ta5d6ffdisplay:flex;align-items:center;justify-content:center;min-height:80vh;}Ta5d6ff"
Ta5d6ff"Ta5d6ff.ok{background:#16213e;padding:30px;border-radius:12px;text-align:center;max-width:400px;}Ta5d6ff"
Ta5d6ff"Ta5d6ffh1{color:#4caf50;margin-bottom:16px;}Ta5d6ff"
Ta5d6ff"Ta5d6ffp{color:#aaa;}Ta5d6ff"
Ta5d6ff"Ta5d6ff</style></head><body>Ta5d6ff"
Ta5d6ff"Ta5d6ff<div class='ok'>Ta5d6ff"
Ta5d6ff"Ta5d6ff<h1>&#x2705; Configuration Saved</h1>Ta5d6ff"
Ta5d6ff"Ta5d6ff<p>Device will reboot in 3 seconds and connect to your WiFi network.</p>Ta5d6ff"
Ta5d6ff"Ta5d6ff<p style='color:#666;font-size:0.85em;'>If the device cannot connect, hold the button for 5+ seconds to re-enter setup.</p>Ta5d6ff"
Ta5d6ff"Ta5d6ff</div></body></html>Ta5d6ff"
Tb4b4b4)Tb4b4b4;
Te6edf3config_serverTff7b72-Tff7b72>Te6edf3sendTb4b4b4(T79c0ff200Tb4b4b4, Ta5d6ff"Ta5d6fftext/htmlTa5d6ff"Tb4b4b4, Te6edf3okTb4b4b4)Tb4b4b4;

T8b949e// Give the response time to send
Te6edf3delayTb4b4b4(T79c0ff3000Tb4b4b4)Tb4b4b4;

T8b949e// Reboot
Te6edf3ESPTb4b4b4.Te6edf3restartTb4b4b4(Tb4b4b4)Tb4b4b4;
Tb4b4b4}

T8b949e// ─── Captive Portal redirect ─────────────────────────────────────────────────
Tff7b72static Tffa657void Td2a8ffconfig_handle_redirectTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3config_serverTff7b72-Tff7b72>Te6edf3sendHeaderTb4b4b4(Ta5d6ff"Ta5d6ffLocationTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffhttp://10.0.0.1/Ta5d6ff"Tb4b4b4, Tffa657trueTb4b4b4)Tb4b4b4;
Te6edf3config_serverTff7b72-Tff7b72>Te6edf3sendTb4b4b4(T79c0ff302Tb4b4b4, Ta5d6ff"Ta5d6fftext/plainTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffRedirecting to setup...Ta5d6ff"Tb4b4b4)Tb4b4b4;
Tb4b4b4}

T8b949e// ─── Check if config is needed ───────────────────────────────────────────────
Tffa657bool Td2a8ffboundary_needs_configTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Check if WiFi SSID is configured
Tffa657char Te6edf3ssidTb4b4b4[T79c0ff33Tb4b4b4]Tb4b4b4;
Tff7b72for Tb4b4b4(Tffa657int Te6edf3i Tff7b72= T79c0ff0Tb4b4b4; Te6edf3i Tff7b72< T79c0ff32Tb4b4b4; Te6edf3iTff7b72+Tff7b72+Tb4b4b4) Tb4b4b4{
Te6edf3ssidTb4b4b4[Te6edf3iTb4b4b4] Tff7b72= Te6edf3EEPROMTb4b4b4.Te6edf3readTb4b4b4(Te6edf3config_addrTb4b4b4(Te6edf3ADDR_CONF_SSID Tff7b72+ Te6edf3iTb4b4b4)Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3ssidTb4b4b4[Te6edf3iTb4b4b4] Tff7b72=Tff7b72= Tb4b4b4(Tffa657charTb4b4b4)T79c0ff0xFFTb4b4b4) Te6edf3ssidTb4b4b4[Te6edf3iTb4b4b4] Tff7b72= Ta5d6ff'Ta5d6ff\0Ta5d6ff'Tb4b4b4;
Tb4b4b4}
Te6edf3ssidTb4b4b4[T79c0ff32Tb4b4b4] Tff7b72= Ta5d6ff'Ta5d6ff\0Ta5d6ff'Tb4b4b4;

T8b949e// Also check boundary mode enable flag
Tffa657uint8_t Te6edf3bmode Tff7b72= Te6edf3EEPROMTb4b4b4.Te6edf3readTb4b4b4(Te6edf3config_addrTb4b4b4(Te6edf3ADDR_CONF_BMODETb4b4b4)Tb4b4b4)Tb4b4b4;

T8b949e// Need config if no SSID set and boundary not yet configured
Tff7b72if Tb4b4b4(Te6edf3ssidTb4b4b4[T79c0ff0Tb4b4b4] Tff7b72=Tff7b72= Ta5d6ff'Ta5d6ff\0Ta5d6ff' Tff7b72&Tff7b72& Te6edf3bmode Tff7b72!Tff7b72= Te6edf3BOUNDARY_ENABLE_BYTETb4b4b4) Tb4b4b4{
Tff7b72return Tffa657trueTb4b4b4;
Tb4b4b4}
Tff7b72return Tffa657falseTb4b4b4;
Tb4b4b4}

T8b949e// ─── Start Config Portal ─────────────────────────────────────────────────────
Tffa657void Td2a8ffconfig_portal_startTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3config_portal_activeTb4b4b4) Tff7b72returnTb4b4b4;

Te6edf3SerialTb4b4b4.Te6edf3printlnTb4b4b4(Ta5d6ff"Ta5d6ff[Config] Starting configuration portal...Ta5d6ff"Tb4b4b4)Tb4b4b4;

T8b949e// Stop any existing WiFi
Te6edf3WiFiTb4b4b4.Te6edf3softAPdisconnectTb4b4b4(Tffa657trueTb4b4b4)Tb4b4b4;
Te6edf3WiFiTb4b4b4.Te6edf3disconnectTb4b4b4(Tffa657trueTb4b4b4, Tffa657trueTb4b4b4)Tb4b4b4;
Te6edf3WiFiTb4b4b4.Te6edf3modeTb4b4b4(Te6edf3WIFI_MODE_NULLTb4b4b4)Tb4b4b4;
Te6edf3delayTb4b4b4(T79c0ff100Tb4b4b4)Tb4b4b4;

T8b949e// Start AP
Te6edf3WiFiTb4b4b4.Te6edf3modeTb4b4b4(Te6edf3WIFI_APTb4b4b4)Tb4b4b4;
Te6edf3WiFiTb4b4b4.Te6edf3softAPTb4b4b4(Te6edf3CONFIG_AP_SSIDTb4b4b4, Tffa657NULLTb4b4b4)Tb4b4b4; T8b949e// Open AP for easy setup
Te6edf3delayTb4b4b4(T79c0ff150Tb4b4b4)Tb4b4b4;

Te6edf3IPAddress Te6edf3ap_addrTb4b4b4(T79c0ff10Tb4b4b4, T79c0ff0Tb4b4b4, T79c0ff0Tb4b4b4, T79c0ff1Tb4b4b4)Tb4b4b4;
Te6edf3IPAddress Te6edf3ap_maskTb4b4b4(T79c0ff255Tb4b4b4, T79c0ff255Tb4b4b4, T79c0ff255Tb4b4b4, T79c0ff0Tb4b4b4)Tb4b4b4;
Te6edf3WiFiTb4b4b4.Te6edf3softAPConfigTb4b4b4(Te6edf3ap_addrTb4b4b4, Te6edf3ap_addrTb4b4b4, Te6edf3ap_maskTb4b4b4)Tb4b4b4;

Te6edf3SerialTb4b4b4.Te6edf3printTb4b4b4(Ta5d6ff"Ta5d6ff[Config] AP started: Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3SerialTb4b4b4.Te6edf3printlnTb4b4b4(Te6edf3CONFIG_AP_SSIDTb4b4b4)Tb4b4b4;
Te6edf3SerialTb4b4b4.Te6edf3printTb4b4b4(Ta5d6ff"Ta5d6ff[Config] IP: Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3SerialTb4b4b4.Te6edf3printlnTb4b4b4(Te6edf3WiFiTb4b4b4.Te6edf3softAPIPTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4;

T8b949e// Start DNS server for captive portal (redirect all domains to us)
Te6edf3config_dns Tff7b72= Te6edf3new Te6edf3DNSServerTb4b4b4(Tb4b4b4)Tb4b4b4;
Te6edf3config_dnsTff7b72-Tff7b72>Te6edf3startTb4b4b4(Te6edf3DNS_PORTTb4b4b4, Ta5d6ff"Ta5d6ff*Ta5d6ff"Tb4b4b4, Te6edf3ap_addrTb4b4b4)Tb4b4b4;

T8b949e// Start web server
Te6edf3config_server Tff7b72= Te6edf3new Te6edf3WebServerTb4b4b4(Te6edf3HTTP_PORTTb4b4b4)Tb4b4b4;
Te6edf3config_serverTff7b72-Tff7b72>Te6edf3onTb4b4b4(Ta5d6ff"Ta5d6ff/Ta5d6ff"Tb4b4b4, Te6edf3HTTP_GETTb4b4b4, Te6edf3config_send_htmlTb4b4b4)Tb4b4b4;
Te6edf3config_serverTff7b72-Tff7b72>Te6edf3onTb4b4b4(Ta5d6ff"Ta5d6ff/saveTa5d6ff"Tb4b4b4, Te6edf3HTTP_POSTTb4b4b4, Te6edf3config_handle_saveTb4b4b4)Tb4b4b4;
Te6edf3config_serverTff7b72-Tff7b72>Te6edf3onNotFoundTb4b4b4(Te6edf3config_handle_redirectTb4b4b4)Tb4b4b4; T8b949e// Captive portal catch-all
Te6edf3config_serverTff7b72-Tff7b72>Te6edf3beginTb4b4b4(Tb4b4b4)Tb4b4b4;

Te6edf3config_portal_active Tff7b72= Tffa657trueTb4b4b4;

Te6edf3SerialTb4b4b4.Te6edf3printlnTb4b4b4(Ta5d6ff"Ta5d6ff[Config] Portal ready — connect to WiFi: Ta5d6ff" Tff7b72+ Te6edf3StringTb4b4b4(Te6edf3CONFIG_AP_SSIDTb4b4b4)Tb4b4b4)Tb4b4b4;

Tff7b72#Tff7b72if HAS_DISPLAY
Tff7b72if Tb4b4b4(Te6edf3disp_readyTb4b4b4) Tb4b4b4{
T8b949e// Show config mode on display
Te6edf3stat_areaTb4b4b4.Te6edf3fillScreenTb4b4b4(Te6edf3SSD1306_BLACKTb4b4b4)Tb4b4b4;
Te6edf3stat_areaTb4b4b4.Te6edf3setCursorTb4b4b4(T79c0ff0Tb4b4b4, T79c0ff0Tb4b4b4)Tb4b4b4;
Te6edf3stat_areaTb4b4b4.Te6edf3printlnTb4b4b4(Ta5d6ff"Ta5d6ffCONFIG MODETa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3stat_areaTb4b4b4.Te6edf3printlnTb4b4b4(Ta5d6ff"Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3stat_areaTb4b4b4.Te6edf3printlnTb4b4b4(Ta5d6ff"Ta5d6ffConnect to:Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3stat_areaTb4b4b4.Te6edf3printlnTb4b4b4(Te6edf3CONFIG_AP_SSIDTb4b4b4)Tb4b4b4;
Te6edf3stat_areaTb4b4b4.Te6edf3printlnTb4b4b4(Ta5d6ff"Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3stat_areaTb4b4b4.Te6edf3printlnTb4b4b4(Ta5d6ff"Ta5d6ffOpen browserTa5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3stat_areaTb4b4b4.Te6edf3printlnTb4b4b4(Ta5d6ff"Ta5d6ffhttp://10.0.0.1Ta5d6ff"Tb4b4b4)Tb4b4b4;
Te6edf3displayTb4b4b4.Te6edf3clearDisplayTb4b4b4(Tb4b4b4)Tb4b4b4;
Te6edf3displayTb4b4b4.Te6edf3drawBitmapTb4b4b4(T79c0ff0Tb4b4b4, T79c0ff0Tb4b4b4, Te6edf3stat_areaTb4b4b4.Te6edf3getBufferTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3stat_areaTb4b4b4.Te6edf3widthTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3stat_areaTb4b4b4.Te6edf3heightTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3SSD1306_WHITETb4b4b4, Te6edf3SSD1306_BLACKTb4b4b4)Tb4b4b4;
Te6edf3displayTb4b4b4.Te6edf3displayTb4b4b4(Tb4b4b4)Tb4b4b4;
Tb4b4b4}
Tff7b72#Tff7b72endif
Tb4b4b4}

T8b949e// ─── Stop Config Portal ──────────────────────────────────────────────────────
Tffa657void Td2a8ffconfig_portal_stopTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3config_portal_activeTb4b4b4) Tff7b72returnTb4b4b4;

Te6edf3SerialTb4b4b4.Te6edf3printlnTb4b4b4(Ta5d6ff"Ta5d6ff[Config] Stopping configuration portalTa5d6ff"Tb4b4b4)Tb4b4b4;

Tff7b72if Tb4b4b4(Te6edf3config_serverTb4b4b4) Tb4b4b4{
Te6edf3config_serverTff7b72-Tff7b72>Te6edf3stopTb4b4b4(Tb4b4b4)Tb4b4b4;
Te6edf3delete Te6edf3config_serverTb4b4b4;
Te6edf3config_server Tff7b72= Tffa657nullptrTb4b4b4;
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3config_dnsTb4b4b4) Tb4b4b4{
Te6edf3config_dnsTff7b72-Tff7b72>Te6edf3stopTb4b4b4(Tb4b4b4)Tb4b4b4;
Te6edf3delete Te6edf3config_dnsTb4b4b4;
Te6edf3config_dns Tff7b72= Tffa657nullptrTb4b4b4;
Tb4b4b4}

Te6edf3WiFiTb4b4b4.Te6edf3softAPdisconnectTb4b4b4(Tffa657trueTb4b4b4)Tb4b4b4;
Te6edf3WiFiTb4b4b4.Te6edf3modeTb4b4b4(Te6edf3WIFI_MODE_NULLTb4b4b4)Tb4b4b4;
Te6edf3config_portal_active Tff7b72= Tffa657falseTb4b4b4;
Tb4b4b4}

T8b949e// ─── Portal Loop — call from main loop() ─────────────────────────────────────
Tffa657void Td2a8ffconfig_portal_loopTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3config_portal_activeTb4b4b4) Tff7b72returnTb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3config_dnsTb4b4b4) Te6edf3config_dnsTff7b72-Tff7b72>Te6edf3processNextRequestTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3config_serverTb4b4b4) Te6edf3config_serverTff7b72-Tff7b72>Te6edf3handleClientTb4b4b4(Tb4b4b4)Tb4b4b4;
Tb4b4b4}

T8b949e// ─── Is portal active? ──────────────────────────────────────────────────────
Tffa657bool Td2a8ffconfig_portal_is_activeTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72return Te6edf3config_portal_activeTb4b4b4;
Tb4b4b4}

Tff7b72#Tff7b72endif T8b949e// BOUNDARY_MODE
Tff7b72#Tff7b72endif T8b949e// BOUNDARY_CONFIG_H

Served by rngit 1.5.0 - Generated in 0.05s